/*
 *  Copyright 2004 Blandware (http://www.blandware.com)
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.blandware.atleap.webapp.taglib.core.content;

import com.blandware.atleap.common.Constants;
import com.blandware.atleap.model.core.ContentField;
import com.blandware.atleap.model.core.Layout;
import com.blandware.atleap.model.core.Page;
import com.blandware.atleap.service.core.ContentFieldManager;
import com.blandware.atleap.service.core.LayoutManager;
import com.blandware.atleap.service.core.PageManager;
import com.blandware.atleap.webapp.struts.ContentTilesRequestProcessor;
import com.blandware.atleap.webapp.struts.HeritableComponentDefinition;
import com.blandware.atleap.webapp.util.core.ApplicationResources;
import com.blandware.atleap.webapp.util.core.CacheUtil;
import com.blandware.atleap.webapp.util.core.WebappConstants;
import com.blandware.atleap.webapp.util.core.WebappUtil;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.struts.Globals;
import org.apache.struts.taglib.TagUtils;
import org.apache.struts.tiles.ComponentDefinition;
import org.apache.struts.tiles.TilesUtil;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.jsp.JspTagException;
import javax.servlet.jsp.PageContext;
import javax.servlet.jsp.tagext.JspFragment;
import javax.servlet.jsp.tagext.SimpleTagSupport;
import java.io.IOException;
import java.util.*;

/**
 * <p>Tag for iterating content field which identifier has special postfix (like
 * array) e.g. cfidentifier[2].
 * <br />
 * The index of content field is of double type.
 * </p>
 * <p>
 * Firstly, a set of content fields is calculated: those are all fields that
 * belong to given definition and page with given URI. Then, among them, fields
 * with given identifier are taken. After that, for each of those fields a
 * content field value with given content locale is taken. So we get a set of
 * content field values; it's then being iterated over, from the least field
 * index to the most one.
 * </p>
 * <p><em>If URI is specified explicitly (through <b>uri</b> attribute), only
 * fields from page are considered: nothing is searched on layouts.</em></p>
 * <p>
 * For each index exports it to specified variable and renders body. If there
 * are no indices at all and <b>ignore</b> is true, body is rendered one time.
 * If there are no indices and <b>ignore</b> is false and site edit mode is ON
 * and <b>createDivAllowed</b>, then DIV is rendered with a message like
 * 'double-click here to create index'; its double-click leads to action which
 * creates an indexed field.
 * </p>
 * <p>
 * <ul>
 * <li>
 * <b>identifier</b> - part of identifier of field before '[' symbol
 * </li>
 * <li>
 * <b>definition</b> - name of tiles definition to which a field may belong. If
 * not specified, definition of current page is taken
 * </li>
 * <li>
 * <b>uri</b> - URI of page to which a field may belong. If not specified, URI
 * of current page is taken.
 * If this attribute is specified, fields are not searched on layouts.
 * </li>
 * <li>
 * <b>locale</b> - identifier of content locale to use. If not specified, user
 * locale is tried. If it's <code>null</code>, default locale is taken.
 * </li>
 * <li>
 * <b>index</b> - name of page scope variable that will accept index of current
 * field on each iteration
 * </li>
 * <li>
 * <b>ignore</b> - If "true", then: 1) errors will be ignored (for
 * instance, when no definition is found at all, or no page with given URI
 * exists); 2) if no indices were found, tag body will still be rendered 1 time.
 * <br/>
 * If "false", then: 1) errors will not be ignored, exceptions will be thrown
 * on errors; 2) if no indices were found, then if site edit mode is ON and
 * <b>createDivAllowed</b>, DIV will be rendered which allows to create an
 * indexed field on double-click.
 * <br/>
 * This can be "true" or "false", default is "false".
 * </li>
 * <li>
 * <b>createDivAllowed</b> - If true, then DIV which allows creation of field
 * index will be rendered if site edit mode is ON and <b>ignore</b> is "false"
 * and no indices were found to iterate over.
 * </li>
 * <li>
 * <b>fieldType</b> - Type of field to be created if nothing was found to
 * iterate over in edit mode. This attribute is used ONLY when field is created
 * by double-click, not when content is obtained. This can be one of following:
 * ContentField.LINE_TYPE, ContentField.MULTILINE_TYPE, ContentField.HTML_TYPE.
 * Default is ContentField.HTML_TYPE.
 * </li>
 * </ul>
 * </p>
 * <p>
 * Here's an example:
 * <pre>
 * &lt;atleap:contentIterator identifier="leftBlock" index="index"&gt;
 *     &lt;div class="block"&gt;
 *         &lt;atleap:content identifier="leftBlock" index="${index}"/&gt;
 *     &lt;/div&gt;
 * &lt;/atleap:contentIterator&gt;
 * </pre>
 * It will render content field values that belong to indexed field with
 * identifier "leftBlock", which is owned by current page or its layout for
 * current content locale.
 * </p>
 * <p><a href="ContentIteratorTag.java.html"><i>View Source</i></a></p>
 *
 * @author Andrey Grebnev <a href="mailto:andrey.grebnev@blandware.com">&lt;andrey.grebnev@blandware.com&gt;</a>
 * @author Sergey Zubtsovskiy <a href="mailto:sergey.zubtsovskiy@blandware.com">&lt;sergey.zubtsovskiy@blandware.com&gt;</a>
 * @author Roman Puchkovskiy <a href="mailto:roman.puchkovskiy@blandware.com">
 *         &lt;roman.puchkovskiy@blandware.com&gt;</a>
 * @version $Revision: 1.30 $ $Date: 2008/01/14 11:19:19 $
 * @jsp.tag name="contentIterator"
 * body-content="scriptless"
 */
public class ContentIteratorTag extends SimpleTagSupport {

	protected transient final Log log = LogFactory.getLog(ContentIteratorTag.class);
    protected static final String NUMBER_KEY = "com.blandware.atleap.taglib.content.CONTENT_ITERATOR_TAG_NUMBER";

    protected ApplicationContext applicationCtx = null;

	/**
	 * Name of variable to export index
	 */
	protected String index;

    /**
	 * Identifier prefix of ContentField
	 */
	protected String identifier = null;
    /**
     * Name of tiles definition to which a field may belong. If not specified,
     * definition of current page is taken
     */
	protected String definition = null;
    /**
     * URI of page to which a field may belong. If not specified, URI of current
     * page is taken
     */
	protected String uri = null;
    /**
     * Identifier of content locale to use. If not specified, user locale is
     * tried. If it's <code>null</code>, default locale is taken.
     */
	protected String locale = null;
    /**
     * Whether errors like absent definition/page will be considered errors or
     * not; also influences what will be done if no indices were found for
     * iterating
     */
	protected Boolean ignore = Boolean.FALSE;
    /**
     * Whether tag is allowed to draw a DIV in edit mode if nothing was found
     * to iterate over
     */
    protected Boolean createDivAllowed = Boolean.TRUE;
    /**
     * Type of field to use when creating first field index
     */
    protected Byte fieldType = null;

    protected Iterator fieldIndicesIterator = null;


	/**
     * Returns identifier of CF
     *
	 * @return identifier
	 * @jsp.attribute required="true"
	 * rtexprvalue="true"
	 * type="java.lang.String"
	 * description="Identifier prefix of content field value"
	 */
	public String getIdentifier() {
		return identifier;
	}

    /**
     * Sets identifier of CF
     *
     * @param identifier identifier to set
     */
	public void setIdentifier(String identifier) {
		this.identifier = identifier;
	}

	/**
     * Returns URI of page to which the field belongs
     *
     * @return URI of owner
	 * @jsp.attribute required="false"
	 * rtexprvalue="true"
	 * type="java.lang.String"
	 * description="URI of the page in database. By default it is uri of current processed page."
	 */
	public String getUri() {
		return uri;
	}

    /**
     * Sets URI of page to which the field belongs
     *
     * @param uri URI of owner to set
     */
	public void setUri(String uri) {
		this.uri = uri;
	}

	/**
     * Returns tiles definition name of layout to which the field belogns
     *
     * @return tiles definition name
	 * @jsp.attribute required="false"
	 * rtexprvalue="true"
	 * type="java.lang.String"
	 * description="Definition name in database. By default it is definition of current processed page."
	 */
	public String getDefinition() {
		return definition;
	}

    /**
     * Sets tiles definition name of layout to which the field belogns
     *
     * @param definition tiles definition name to set
     */
	public void setDefinition(String definition) {
		this.definition = definition;
	}

	/**
     * Returns name of variable to export index
     *
     * @return name of variable to export index
	 * @jsp.attribute required="true"
	 * rtexprvalue="true"
	 * type="java.lang.String"
	 * description="Name of variable to export index"
	 */
	public String getIndex() {
		return index;
	}

    /**
     * Sets name of variable to export index
     *
     * @param index name of variable to export index to set
     */
	public void setIndex(String index) {
		this.index = index;
	}

	/**
     * Returns locale
     *
     * @return locale
	 * @jsp.attribute required="false"
	 * rtexprvalue="true"
	 * type="java.lang.Boolean"
	 * description="By default locale is used from session"
	 */
	public String getLocale() {
		return locale;
	}

    /**
     * Sets locale
     *
     * @param locale locale to set
     */
	public void setLocale(String locale) {
		this.locale = locale;
	}

	/**
     * Returns whether to ignore errors
     *
     * @return whether to ignore errors
	 * @jsp.attribute required="false"
	 * rtexprvalue="true"
	 * type="java.lang.Boolean"
	 * description="If it is true and there are not fields the tag body will be shown one time"
	 */
	public Boolean getIgnore() {
		return ignore;
	}

    /**
     * Sets whether to ignore errors
     *
     * @param ignore whether to ignore errors
     */
	public void setIgnore(Boolean ignore) {
		this.ignore = ignore;
	}

	/**
     * Returns whether tag is allowed to draw in edit mode a DIV used to create
     * a field when no indices found to iterate over.
     *
     * @return whether tag is allowed to draw a DIV
	 * @jsp.attribute required="false"
	 * rtexprvalue="true"
	 * type="java.lang.Boolean"
	 * description="Whether tag is allowed to draw a DIV used to create a field when no indices found to iterate over"
	 */
    public Boolean getCreateDivAllowed() {
        return createDivAllowed;
    }

    /**
     * Sets whether tag is allowed to draw in edit mode a DIV used to create
     * a field when no indices found to iterate over.
     *
     * @param createDivAllowed value to set
     */
    public void setCreateDivAllowed(Boolean createDivAllowed) {
        this.createDivAllowed = createDivAllowed;
    }

    /**
     * Returns type of field to use when creating first field index (in
     * edit mode).
     *
     * @return field type
     * @jsp.attribute required="false"
	 * rtexprvalue="true"
	 * type="java.lang.Byte"
	 * description="Type of field to use when creating first field index (in edit mode)"
     */
    public Byte getFieldType() {
        return fieldType;
    }

    /**
     * Sets type of field to use when creating first field index (in edit mode).
     *
     * @param fieldType type to set
     */
    public void setFieldType(Byte fieldType) {
        this.fieldType = fieldType;
    }

    /**
	 * Constructs an iterator for indexed looping through the body once per
     * element.
	 *
	 * @throws javax.servlet.jsp.JspTagException
	 *          if a JSP exception has occurred
	 */
	public void doTag() throws JspTagException, IOException {

		PageContext pageContext = (PageContext) getJspContext();
		HttpServletRequest request = (HttpServletRequest) pageContext.getRequest();
		ServletContext servletContext = request.getSession().getServletContext();

		try {

			if ( log.isDebugEnabled() ) {
				log.debug("Start processing content fields.");
			}

			if ( applicationCtx == null ) {
				applicationCtx = WebApplicationContextUtils.getRequiredWebApplicationContext(pageContext.getServletContext());
			}

            if (createDivAllowed == null) {
                createDivAllowed = Boolean.TRUE;
            }

            //get locale
			if ( locale == null ) {
				Locale l = (Locale) pageContext.getAttribute(Globals.LOCALE_KEY, PageContext.SESSION_SCOPE);
				if ( l != null ) {
					locale = l.getLanguage();
				}
			}
			if ( locale == null ) {
				locale = Locale.getDefault().getLanguage();
			}

			// get definition
			if ( definition != null ) {
				ComponentDefinition tmpDef = null;
				try {
					tmpDef = TilesUtil.getDefinition(definition, request, servletContext);
				} catch ( Exception ex ) {
					//do nothing
				}
				if ( tmpDef == null ) {
					if ( ignore.booleanValue() ) {
						return;
					}
					String errorMessage = "Specified definition '" + definition + "' has not been found";
                    throw new JspTagException(errorMessage);
				}
			} else {
				definition = (String) pageContext.getAttribute(ContentTilesRequestProcessor.DEFINITION_NAME, PageContext.REQUEST_SCOPE);
			}
			if ( definition == null ) {
				if ( ignore.booleanValue() ) {
					return;
				}
				String errorMessage = "DEFINITION_NAME attribute has not been found in request. This can be used only on pages processed by action using tile definition.";
                throw new JspTagException(errorMessage);
			}

			//get uri
            boolean uriGiven = uri != null;
            if ( uri == null ) {
                uri = (String) pageContext.getAttribute(ContentTilesRequestProcessor.PROCESSED_URI, PageContext.REQUEST_SCOPE);
            }
			if ( uri == null ) {
				if ( ignore.booleanValue() ) {
					return;
				}
				String errorMessage = "PROCESSED_URI attribute has not been found in request. This can be used only on pages processed by action using tile definition.";
                throw new JspTagException(errorMessage);
			}

            // calculate tag number
            Integer tagNumber = (Integer) pageContext.getAttribute(NUMBER_KEY, PageContext.REQUEST_SCOPE);
            if ( tagNumber == null ) {
                tagNumber = new Integer(0);
            } else {
                tagNumber = new Integer(tagNumber.intValue() + 1);
            }

            pageContext.setAttribute(NUMBER_KEY, tagNumber, PageContext.REQUEST_SCOPE);

            CacheUtil cacheUtil = CacheUtil.getInstance(request);

			String[] fieldIndices;
			fieldIndices = cacheUtil.getFieldIndices(identifier, locale, definition, uri);

			if ( fieldIndices == null ) {
				//cache is empty or stale - try to get from database
				if ( log.isDebugEnabled() ) {
					log.debug("Trying to search in database");
				}

				ContentFieldManager contentFieldManager = (ContentFieldManager) applicationCtx.getBean(Constants.CONTENT_FIELD_MANAGER_BEAN);
				LayoutManager layoutManager = (LayoutManager) applicationCtx.getBean(Constants.LAYOUT_MANAGER_BEAN);
				Collection allFields = null;

                if (!uriGiven) {
                    List layouts = new ArrayList();
                    String tmpDefinition = definition;
                    do {
                        Layout layout = layoutManager.findLayoutByDefinition(tmpDefinition);
                        if ( layout != null ) {
                            layouts.add(layout);
                        }
                        tmpDefinition = ((HeritableComponentDefinition) TilesUtil.getDefinition(tmpDefinition, request, servletContext)).getExtends();
                    } while ( tmpDefinition != null );

                    Collections.reverse(layouts);
                    for ( int i = 0; i < layouts.size(); i++ ) {
                        Layout layout = (Layout) layouts.get(i);
                        List fields = contentFieldManager.findIndexedContentFieldsByLayoutDefinitionAndLocale(identifier, layout.getDefinition(), locale);
                        if ( log.isDebugEnabled() && fields != null ) {
                            log.debug("Using " + fields.size() + " fields from layout with definition: " + layout.getDefinition());
                        }
                        allFields = WebappUtil.joinFields(allFields, fields);
                    }
                }

                List pageFields = contentFieldManager.findIndexedContentFieldsByPageUriAndLocale(identifier, uri, locale);
				if ( log.isDebugEnabled() && pageFields != null ) {
					log.debug("Using " + pageFields.size() + " fields from page with uri: " + uri);
				}
				allFields = WebappUtil.joinFields(allFields, pageFields);

				if ( allFields != null ) {
					List sortedFields = new LinkedList(allFields);
					Collections.sort(sortedFields, new WebappUtil.IndexedContentFieldComparator());

					if ( log.isDebugEnabled() ) {
						log.debug("Found " + allFields.size() + " fields for definition name: " + definition + ", locale: " + locale + ", uri: " + uri + ", identifier: " + identifier);
					}

					List fieldIndicesList = new ArrayList();

					for ( int i = 0; i < sortedFields.size(); i++ ) {
						ContentField contentField = (ContentField) sortedFields.get(i);
						String indexValue = WebappUtil.getContentFieldIndex(contentField.getIdentifier());
						if ( indexValue != null ) {
							fieldIndicesList.add(indexValue);
						}
					}

					fieldIndices = (String[]) fieldIndicesList.toArray(new String[fieldIndicesList.size()]);

					cacheUtil.putFieldIndicesInCache(fieldIndices, identifier, locale, definition, uri);
				}
			}

            if ( (fieldIndices == null || fieldIndices.length == 0) && uriGiven ) {
                // nothing was found, check maybe there's no page with given URI
                PageManager pageManager = (PageManager) applicationCtx.getBean(Constants.PAGE_MANAGER_BEAN);
                Page page = pageManager.findPageByUri(uri);
                if ( page == null ) {
                    if ( ignore.booleanValue() ) {
                        return;
                    }
                    String errorMessage = "Specified page uri '" + uri + "' has not been found";
                    throw new JspTagException(errorMessage);
                }
		    }

            if ( fieldIndices != null ) {
				fieldIndicesIterator = Arrays.asList(fieldIndices).iterator();
			}

			if ( fieldIndicesIterator != null
                    && fieldIndicesIterator.hasNext() ) {
				while ( fieldIndicesIterator.hasNext() ) {
					String indexValue = (String) fieldIndicesIterator.next();
					pageContext.setAttribute(index, indexValue);
					if ( log.isDebugEnabled() ) {
						log.debug("Putting as attribute with key=" + index + " and value=" + indexValue);
					}
					JspFragment body = getJspBody();
					if ( body != null ) {
						body.invoke(null);
					}
				}
			} else {
				Object editModeEnabledAttr = request.getSession().getAttribute(WebappConstants.SITE_EDIT_MODE_ENABLED_KEY);
				boolean editModeEnabled = editModeEnabledAttr != null && Boolean.TRUE.equals(editModeEnabledAttr);
                if ( ignore.booleanValue() ) {
                    JspFragment body = getJspBody();
					if ( body != null ) {
						body.invoke(null);
					}
				} else if (editModeEnabled && createDivAllowed.booleanValue()) {
                    Long contentPageId = (Long) request.getAttribute(ContentTilesRequestProcessor.CONTENT_PAGE_ID);
                    StringBuffer divId = new StringBuffer("__empty__iterator__")
                            .append(tagNumber).append("__wrapper__");
                    String requestUrl = (String) request.getAttribute(ContentTilesRequestProcessor.PROCESSED_URL);
                    if ( requestUrl == null ) {
                        requestUrl = WebappUtil.findGlobalForwardConfig("index", null, request).getPath();
                    }

                    if (fieldType == null
                            || (fieldType.byteValue() != ContentField.LINE_TYPE
                            && fieldType.byteValue() != ContentField.MULTILINE_TYPE
                            && fieldType.byteValue() != ContentField.HTML_TYPE)) {
                        fieldType = new Byte(ContentField.HTML_TYPE);
                    }

                    StringBuffer onmouseover = new StringBuffer("doSelectLayer(this.id);");
                    StringBuffer onmouseout = new StringBuffer("doUnselectLayer(this.id);");
                    StringBuffer ondblclick = new StringBuffer("doCallCreateIndexedField('")
                            .append(contentPageId).append("', '")
                            .append(identifier).append("', ")
                            .append(0).append(", ")
                            .append(fieldType).append(", '")
                            .append(definition).append("', '")
                            .append(requestUrl).append("');");

                    TagUtils tagUtils = TagUtils.getInstance();
                    ApplicationResources applicationResources = ApplicationResources.getInstance(servletContext);

                    String divDisplay = "block";
                    String message = applicationResources.getMessage(request,
                            "core.commons.contentIterator.fieldsNotFound",
                            new Object[]{identifier, uri, definition});

                    String content = new StringBuffer("<div id=\"")
                            .append(divId).append("\" name=\"").append(divId)
                            .append("\" class=\"fieldValueWrapper\" style=\"display: ")
                            .append(divDisplay).append(";\" ")
                            .append("onmouseover=\"").append(onmouseover)
                            .append("\" onmouseout=\"").append(onmouseout)
                            .append("\" ondblclick=\"").append(ondblclick)
                            .append("\">").append(message).append("</div>")
                            .toString();

                    tagUtils.write(pageContext, content);
                }
            }

		} catch ( Exception ex ) {
			if ( ex instanceof IOException ) {
				throw (IOException) ex;
			}
            throw new JspTagException(ex);
		}

		if ( index != null ) {
			pageContext.removeAttribute(index);
		}

	}

}
